<!doctype html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Document</title>
	<link href="https://cdn.bootcdn.net/ajax/libs/normalize/8.0.1/normalize.min.css" rel="stylesheet">
	<style type="text/css">
	body {
		line-height: 1.4;
		font-size: 16px;
	}
	article p {margin: 5px 0 10px 0;}
	img { vertical-align: top; max-width: 100%;}
	pre {
	    margin: 20px 0;
	    font: 12px/20px 'courier new';
	    background: #4A4A4A;
	    padding: 10px 20px;
	    color: #F8F8D4;
	    border-radius: 0;
	    border: none;
	}
	pre code {
		padding: 0;
	    color: inherit;
	    white-space: pre;
	    white-space: pre-wrap;
	    background-color: transparent;
	    border: 0;
	}
	</style>
</head>
<body>
	<article class="article-content" style="max-width: 960px; margin: auto; padding: 30px 0">
			
<p>这已经是去年的事情了，这件事要从被同事打脸开始说起…</p>



<p>去年我和同事开玩笑，说只要网上你能看到的东西，我就能下载下来让他保存在电脑上。同事摇了摇头，一脸的不相信，然后拿住我的鼠标，打开了众所周知的慕课网，说：“我要这个视频，你能把文件给我吗？”。</p>



<p>我点开浏览器控制台，发现网络请求中一堆的ts文件，之前我又不是没有下载过这样的视频，M3U8而已，怎么能难得住我？我满口就答应了同事，说：“5分钟后给你。”，事后我发现人们总是会低估一件事情的难度，从业者都低估自己所面对的需求，更何况是外行人呢。不过最后我还是用实际行动证明了我的观点，凡是浏览器能播放的东西，都可以保存在自己的电脑上，不管多么的复杂。</p>



<p>虽然我看到了一堆的ts文件，可是我苦苦寻找，并没有找到M3U8文件，那么这传说中的M3U8文件到底去了哪里了呢？</p>



<p>然后我从一套大家都见过的课程开始入手了，慢慢探索这其中的奥秘。</p>



<p>视频的播放地址是：</p>



<pre class="wp-block-code"><code>https://www.imooc.com/video/1430</code></pre>



<p>是个学编程的人都知道后面的1430是个id，那么这个id用来做什么了呢？</p>



<p>看了看网络请求，发现了一个非常有趣的链接：</p>



<pre class="wp-block-code"><code>https://www.imooc.com/course/playlist/1430?t=m3u8&amp;_id=5848c001b3fee30a6c8b51bd&amp;cdn=aliyun1</code></pre>



<p>t=m3u8，嗯？看到了我熟悉的m3u8，那么t可能代表的意思是type喽，后面的_id参数又是什么？毫不犹豫我按下了ctrl + F，搜索一下5848c001b3fee30a6c8b51bd，在搜索结果中我看了了HTML页面上居然有这个东西：</p>



<pre class="wp-block-code"><code>OP_CONFIG.mongo_id="5848c001b3fee30a6c8b51bd";
OP_CONFIG.page="video2.4";</code></pre>



<p>没有问题，就是他了，后面的cdn=aliyun1这个不用想就知道他是用了阿里云的cdn。</p>



<p>链接中的参数都找全了，但是有一个问题，这个链接真的是返回的m3u8文件吗？</p>



<p>看了一眼响应数据，我又傻眼了：</p>



<pre class="wp-block-code"><code>{
    "result":1,
    "data":{
        "info":"MkkqXqhmamoxAUB1PQ...",
        "cdn":["aliyun","aliyun1","letv"]
    },
    "msg":""
}</code></pre>



<p>info中一堆乱七八糟的东西，这堆东西到底是啥??? 明明这个链接看上去好像是要返回M3U8来着，但是为什么返回了这么一堆东西，一定是某种加密方式，不探索出这种加密方式，还怎么往下面进行。可是啊，加密方式的探索，哪里有那么容易。</p>



<p>我想了一会儿，这个链接是哪里发送出来的呢？我就可以从哪里找到他的加密方式啊。我又开始了一顿搜索，在一个js文件中看到了这个链接。</p>



<p>摘录一小部分：</p>



<pre class="wp-block-code"><code>...
"https" === h &amp;&amp; (v = "https://www.imooc.com/course/playlist/" + pageInfo.mid + "?t=m3u8&amp;_id=" + OP_CONFIG.mongo_id),
window.thePlayer = mocoplayer($("#video-box"), {
    url: v,
    title: videoTitle,
...</code></pre>



<p>我们可以看到，拼接好了链接字符串后，又调用了mocoplayer方法，那么我一定可以根据这个mocoplayer方法来一步步跟进，得知他的加密算法的。又一顿搜索，得知了，mocoplayer原来在mocoplayer.js中。不错不错。可是难题来了，这个文件好大啊，使用webpack打包的文件，让我怎么看？</p>



<p>苦想了很久，应该用什么思路去面对这么大的文件呢？突然想到一招，我看看上面那个链接，到底是js的哪一行请求的不就行了？</p>

<p>很快我就到了发起链接请求的地方，可是这也太难找了吧，发起了一个链接请求而已，距离解密可能还早着呢!!!</p>

<p>没有办法，既然到这里了，那我就必须要打着断点往后看，毕竟这个链接一旦访问成功，离成功就不远了啊，过了好长一段时间，我看到了代码中有data.info这几个字符，立刻集中了精神，马上要到了，紧接着我就看到了下面的代码：</p>


<div class="wp-block-image"><figure class="aligncenter"><img src="https://halo.cyblogs.top/upload/2020/2/image-b78893d27a7b43d5b53e948408f65058.png" alt=""><figcaption>慕课网视频解密方法</figcaption></figure></div>

<p>这个被圈起来的部分长得好眼熟，那么就意味着这个destm_1.default(mediadata.data.info);就是解密方法了。</p>



<p>深深的呼吸了一口气，给这个方法打上断点，然后进去看看，果不其然，最终确定这个文件的21919行到22066行是我想要的解密算法。</p>



<p>给这个字符串解密后，我发现，并不是我想的那么简单!!!</p>



<pre class="wp-block-code"><code>#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=512000, RESOLUTION=1280x720
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/medium.m3u8?cdn=aliyun1...
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=384000, RESOLUTION=1280x720
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/medium.m3u8?cdn=aliyun1...
#EXT-X-STREAM-INF:PROGRAM-ID=1, BANDWIDTH=256000, RESOLUTION=720x480
https://www.imooc.com/video/5848c001b3fee30a6c8b51bd/low.m3u8?cdn=aliyun1...</code></pre>



<p>看上去像是M3U8文件，可是文件里面的链接却又是m3u8格式的???</p>



<p>我尝试着访问了这些m3u8地址，于是又看到了熟悉的东西：</p>



<pre class="wp-block-code"><code>{
    "code":200,
    "data":{
        "info":"Gm1kQ7hmVBYXVGoxVGdUVGA9VBwcKnAyF1RmJD1UY1QHVGRrVBxu..."
    },
    "msg":"OK"
}</code></pre>



<p>现实总是那么的出乎意料，这一定又是一个加密后的字符串，我敢确定，一定和上一次用了同样的加密方法，我再次试了试：</p>



<div class="wp-block-image"><figure class="aligncenter"><img src="https://halo.cyblogs.top/upload/2020/2/image-c914653c9fc44fdd9f470cca94efc037.png" alt=""><figcaption>m3u8字符串</figcaption></figure></div>



<p>丝毫没有问题，解密出来了，这就是我想要的m3u8，但是真的有那么简单吗???</p>



<p>我把这些ts文件直接下载下来，可是加着密呢呀!!!</p>



<p>上面M3U8字符串中有一串METHOD=AES-128引起了我的注意，这明显是告诉我，加密方式是用了什么!!! 后面的那一串链接一定是密钥了。</p>



<p>可是我打开链接后，居然与普通的密钥完全不一样，他长这样：</p>



<pre class="wp-block-code"><code>{
    "code":200,
    "data":{
        "info":"I2RqhmmlX1UTHvYGIi4cQKqgARoiHh1rIjIo7WRlwhoT9h1nIg==t01UNGJIRvfj"
    },
    "msg":"OK"
}</code></pre>



<p>历史总是那么惊人的相似，又解了一下密：</p>



<div class="wp-block-image"><figure class="aligncenter"><img src="https://halo.cyblogs.top/upload/2020/2/image-f5c2215b424a4462949232db78b1c1f3.png" alt=""><figcaption>解密字符串</figcaption></figure></div>



<p>看上去，并不是那么回事，后面的参数改成了true，它长成了这样：</p>



<div class="wp-block-image"><figure class="aligncenter"><img src="https://halo.cyblogs.top/upload/2020/2/image-7359b7d8bf3341f4a038a8b2ac5e87af.png" alt=""><figcaption>解密慕课网视频秘密</figcaption></figure></div>



<p>看起来像是那么回事了，正好16个字节，正好符合AES-128也就是传说中的AES/CBC/PKCS5PADDING，好了，java解密代码安排起来：</p>



<pre class="wp-block-code"><code>/**
 * AES-CBC加解密，该类用来对每一段视频进行解密
 *
 * @author CY
 */
public class AESCBCDecrypt {

    private static final String AES = "AES";

    private static final String AES_CBC = "AES/CBC/PKCS5PADDING";

    /**
     * 使用AES加密或解密无编码的原始字节数组, 返回无编码的字节数组结果.
     *
     * @param input 原始字节数组
     * @param key   符合AES要求的密钥
     * @param iv    初始向量
     * @param mode  Cipher.ENCRYPT_MODE 或 Cipher.DECRYPT_MODE
     */
    public static byte[] aes(byte[] input, byte[] key, byte[] iv, int mode) {
        try {
            Cipher cipher = Cipher.getInstance(AES_CBC);
            cipher.init(mode, new SecretKeySpec(key, AES), new IvParameterSpec(iv));
            cipher.update(input);
            return cipher.doFinal(input);
        } catch (GeneralSecurityException e) {
            throw new RuntimeException("加解密出现了异常，当前的key：" + Arrays.toString(key) + "，IV：" + Arrays.toString(iv));
        }
    }
}</code></pre>



<p>可是啊，需要的iv呢，他去哪里了，问题又出来了，我得看看，是哪里发起的ts文件请求，发起请求的地方应该会有iv吧！</p>



<p>又如我所料，找到了。</p>



<div class="wp-block-image"><figure class="aligncenter"><img src="https://halo.cyblogs.top/upload/2020/2/image-d3cda5e469814106902e02af35c3a046.png" alt=""><figcaption>ts文件请求</figcaption></figure></div>



<p>我跟着断点走了好几个ts文件，发现iv前面15位永远都是0，只是第16位会跟随ts文件的变化而发生变化，会逐渐的加1，我也可以模拟这样的变化，用来创建视频的iv值。</p>



<p>经过了一会，我把上面的东西全部梳理了一遍，解密算法是用js写的，而我使用了java，那么我有两种选择：</p>



<ul><li>把它用java代码重新构建一遍（效率高）</li><li>用java来执行js文件（效率低）</li></ul>



<p>java执行js的代码如下：</p>



<pre class="wp-block-code"><code>/**
 * 解码的备用方法，运行JavaScript脚本，速度较慢
 *
 * @author CY
 */
public class ImoocDecoderBackup {

    /**
     * JavaScript解码算法执行器
     */
    private static Invocable invocable;

    /* 读取JS文件，并构造执行器 */
    static {
        InputStream inputStream = ImoocDecoderBackup.class.getClassLoader().getResourceAsStream("attachment/decode.js");
        if (inputStream != null) {
            try {
                String javaScript = new BufferedReader(new InputStreamReader(inputStream)).lines().collect(Collectors.joining("\n"));
                ScriptEngine engine = new ScriptEngineManager().getEngineByName("Nashorn");
                engine.eval(javaScript);
                invocable = (Invocable) engine;
            } catch (ScriptException ignored) {
            }
        }
    }

    /**
     * 解码成字符串
     *
     * @param crypto 待解码的字符串
     * @return 解码后的字符串
     */
    public static String decoderString(String crypto) {
        return invoke("decode2String", crypto);
    }

    /**
     * 获取慕课网视频的密匙key
     *
     * @param crypto 待解码的字符串
     * @return 字节数组
     */
    public static byte[] decoderKey(String crypto) {
        return new Gson().fromJson(invoke("decode2Bytes", crypto), byte[].class);
    }

    /**
     * 调用JavaScript方法
     *
     * @param methodName   方法名
     * @param encodeString 待解码的字符串
     * @return 解码后的结果
     */
    private static String invoke(String methodName, String encodeString) {
        try {
            return invocable.invokeFunction(methodName, encodeString).toString();
        } catch (ScriptException | NoSuchMethodException ignored) {
        }
        return null;
    }
}</code></pre>

<p>解密出来之后生成一个m3u8文件，那么我就需要解析这个m3u8了，解析它就要符合HLS协议的规范，我尝试写了一下，并且做了测试，但是我发现，我写的并不那么理想，适用范围较小，因为毕竟HLS协议也不是那么简单的规定，他除了普通的视频流，还有直播流的，我找了找同性交友网站，看见了一个工具<a href="https://github.com/iheartradio/open-m3u8" target="_blank" rel="noreferrer noopener" aria-label="open-m3u8（在新窗口打开）">open-m3u8</a>，不管它好不好用，反正我不用，既然自己写了一部分，就用自己的。</p>

<p>上面的一切工作准备就绪了，现在已经拥有了，解密，解析，那么接下来就是普普通通的下载了，边下载边进行AES-128解密就可以了，可是下载下来的文件零零散散，我还需要合并的，刚开始的时候使用Java的输出流拼接的方式进行的合并，可是没多久就发现了这个方式并不是那么的理想。因为视频也有头和尾，直接硬生生的拼接在一起，相当于玩了一场人体蜈蚣，播放的时候会造成一定的卡顿现象，于是我就选择了使用FFMpeg的方式进行合并：</p>

<pre class="wp-block-code"><code>./ffmpeg.exe -i "concat:ts文件1|ts文件2" -c copy -bsf:a aac_adtstoasc "输出文件"</code></pre>

<p>最终解密出来了一堆完整的视频，放一张成果图片：</p>

<div class="wp-block-image"><figure class="aligncenter"><img src="https://halo.cyblogs.top/upload/2020/2/image-d0f4bf2a272f4ecbb65a16c8c7681c4f.png" alt=""><figcaption>下载慕课网视频</figcaption></figure></div>

<p>推荐阅读：https://halo.cyblogs.top/archives/decrypt-imooc-video-download.html 和 https://www.xttblog.com/?p=4996 和 https://github.com/iheartradio/open-m3u8</p>
		</article>
</body>
</html>